Repository: quantumlib/Cirq

Issue:
IonQ Job should throw TimeoutError when wait time exceeded
**Description of the issue**

Currently, `Job.results()` throws a `RuntimeError` when the job execution exceeds the `timeout_seconds` wait time. It would be more accurate to throw a `TimeoutError` instead.

see cirq-ionq/cirq_ionq/job.py:

https://github.com/quantumlib/Cirq/blob/7759c05fd71673ca58559307c220b5b779bf5bb6/cirq-ionq/cirq_ionq/job.py#L202

**How to reproduce the issue**

```
import cirq
import cirq.ionq as ionq

API_KEY = 'tomyheart' # Replace with your IonQ API key

service = ionq.Service(api_key=API_KEY, 
                       default_target='simulator')

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
    cirq.X(q0)**0.5,             # Square root of X
    cirq.CX(q0, q1),              # CNOT
    cirq.measure(q0, q1, key='b') # Measure both qubits
)

# create a job
job = service.create_job(circuit, repetitions=100)
job.results(timeout_seconds=0.1) # trigger a timeout
```

<details>

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
/tmp/ipykernel_6204/2129298043.py in <module>
----> 1 job.results(timeout_seconds=0.01)

~/anaconda3/envs/my-env/lib/python3.7/site-packages/cirq_ionq/job.py in results(self, timeout_seconds, polling_seconds)
    199                 raise RuntimeError(f'Job failed. Error message: {error}')
    200             raise RuntimeError(
--> 201                 f'Job was not completed successful. Instead had status: {self.status()}'
    202             )
    203         # IonQ returns results in little endian, Cirq prefers to use big endian, so we convert.

RuntimeError: Job was not completed successful. Instead had status: ready

</details>

**Cirq version**

0.12.0


Discussion:

Resolving patch:
diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py
--- a/cirq-ionq/cirq_ionq/job.py
+++ b/cirq-ionq/cirq_ionq/job.py
@@ -199,8 +199,10 @@ def results(
             if 'failure' in self._job and 'error' in self._job['failure']:
                 error = self._job['failure']['error']
                 raise RuntimeError(f'Job failed. Error message: {error}')
+            if time_waited_seconds >= timeout_seconds:
+                raise TimeoutError(f'Job timed out after waiting {time_waited_seconds} seconds.')
             raise RuntimeError(
-                f'Job was not completed successful. Instead had status: {self.status()}'
+                f'Job was not completed successfully. Instead had status: {self.status()}'
             )
         # IonQ returns results in little endian, Cirq prefers to use big endian, so we convert.
         if self.target() == 'qpu':


Test patch:
diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py
--- a/cirq-ionq/cirq_ionq/job_test.py
+++ b/cirq-ionq/cirq_ionq/job_test.py
@@ -79,6 +79,14 @@ def test_job_results_failed():
     assert job.status() == 'failed'
 
 
+def test_job_results_failed_no_error_message():
+    job_dict = {'id': 'my_id', 'status': 'failed', 'failure': {}}
+    job = ionq.Job(None, job_dict)
+    with pytest.raises(RuntimeError, match='failed'):
+        _ = job.results()
+    assert job.status() == 'failed'
+
+
 def test_job_results_qpu_endianness():
     job_dict = {
         'id': 'my_id',
@@ -124,7 +132,7 @@ def test_job_results_poll_timeout(mock_sleep):
     mock_client = mock.MagicMock()
     mock_client.get_job.return_value = ready_job
     job = ionq.Job(mock_client, ready_job)
-    with pytest.raises(RuntimeError, match='ready'):
+    with pytest.raises(TimeoutError, match='seconds'):
         _ = job.results(timeout_seconds=1, polling_seconds=0.1)
     assert mock_sleep.call_count == 11