Repository: PrefectHQ/prefect
Issue category: Bug Report

`TimedOut` task has no duration or end time
### Bug summary

 Using the following sample flow to produce a `TimedOut` task and flow, the flow run receives a duration and end time but the task run does not.

```python
from prefect import flow, task
import requests

@task(timeout_seconds=5)
def timeout_task():
    requests.get("http://httpbin.org/delay/10")

@flow
def timeout_flow():
    timeout_task()


if __name__ == "__main__":
    timeout_flow()
```

<img width="2159" alt="image" src="https://github.com/user-attachments/assets/a3fab759-1f78-40c9-ad94-f930b681dcfc">

<img width="416" alt="image" src="https://github.com/user-attachments/assets/4c37ae94-bf54-48ae-9e29-8102901c4414">


### Version info

```Text
Version:             3.1.0
API version:         0.8.4
Python version:      3.12.4
Git commit:          a83ba39b
Built:               Thu, Oct 31, 2024 12:43 PM
OS/Arch:             darwin/arm64
Profile:             default
Server type:         cloud
Pydantic version:    2.9.1
```


### Additional context

_No response_


Functions to edit:
src/prefect/task_engine.py:SyncTaskRunEngine.handle_timeout
src/prefect/task_engine.py:AsyncTaskRunEngine.handle_timeout

Patch:
diff --git a/src/prefect/task_engine.py b/src/prefect/task_engine.py
index a07f2108c95a..d6b834d6bca6 100644
--- a/src/prefect/task_engine.py
+++ b/src/prefect/task_engine.py
@@ -600,6 +600,7 @@ def handle_timeout(self, exc: TimeoutError) -> None:
                 message=message,
                 name="TimedOut",
             )
+            self.record_terminal_state_timing(state)
             self.set_state(state)
             self._raised = exc
 
@@ -1134,6 +1135,7 @@ async def handle_timeout(self, exc: TimeoutError) -> None:
                 message=message,
                 name="TimedOut",
             )
+            self.record_terminal_state_timing(state)
             await self.set_state(state)
             self._raised = exc
             self._telemetry.end_span_on_failure(state.message)

Test patch: diff --git a/tests/test_task_engine.py b/tests/test_task_engine.py
index 185045fb9455..c4f1847b160c 100644
--- a/tests/test_task_engine.py
+++ b/tests/test_task_engine.py
@@ -1511,6 +1511,58 @@ async def foo():
         assert run.end_time == failed.timestamp
         assert run.total_run_time == failed.timestamp - running.timestamp
 
+    async def test_sync_task_sets_end_time_on_failed_timedout(
+        self, prefect_client, events_pipeline
+    ):
+        ID = None
+
+        @task
+        def foo():
+            nonlocal ID
+            ID = TaskRunContext.get().task_run.id
+            raise TimeoutError
+
+        with pytest.raises(TimeoutError):
+            run_task_sync(foo)
+
+        await events_pipeline.process_events()
+
+        run = await prefect_client.read_task_run(ID)
+
+        states = await prefect_client.read_task_run_states(ID)
+        running = [state for state in states if state.type == StateType.RUNNING][0]
+        failed = [state for state in states if state.type == StateType.FAILED][0]
+
+        assert failed.name == "TimedOut"
+        assert run.end_time
+        assert run.end_time == failed.timestamp
+        assert run.total_run_time == failed.timestamp - running.timestamp
+
+    async def test_async_task_sets_end_time_on_failed_timedout(
+        self, prefect_client, events_pipeline
+    ):
+        ID = None
+
+        @task
+        async def foo():
+            nonlocal ID
+            ID = TaskRunContext.get().task_run.id
+            raise TimeoutError
+
+        with pytest.raises(TimeoutError):
+            await run_task_async(foo)
+
+        await events_pipeline.process_events()
+        run = await prefect_client.read_task_run(ID)
+        states = await prefect_client.read_task_run_states(ID)
+        running = [state for state in states if state.type == StateType.RUNNING][0]
+        failed = [state for state in states if state.type == StateType.FAILED][0]
+
+        assert failed.name == "TimedOut"
+        assert run.end_time
+        assert run.end_time == failed.timestamp
+        assert run.total_run_time == failed.timestamp - running.timestamp
+
     async def test_sync_task_sets_end_time_on_crashed(
         self, prefect_client, events_pipeline
     ):