Repository: pyvista/pyvista

Issue:
Timestamp extraction from XdmfReader
### Describe the feature you would like to be added.

Hey, guys!

I needed to extract the timestamps from a .xdmf file, but I couldn't find a clear and easy way to do that using pyvista's XdmfReader. I noticed that the XdmfReader class does not inherit from the TimeReader class, so that is probably the reason. 

In the following code snippet, I found a workaround to this issue, which is to call the methods from the vtkXdmfReader original class inside the pyvista reader object. However, this is not an ideal solution. Is there anyway this could be integrated into pyvista in future updates?

```python
import pyvista as pv
import numpy as np
import vtk

file_path = "file.xdmf"

# Create an Xdmf reader
pv_reader = pv.XdmfReader(path=file_path)

# Get the field names
field_names = pv_reader.point_array_names

# Get the output information
info = pv_reader.reader.GetOutputInformation(0)

# Get the time steps
timestamps = info.Get(vtk.vtkCompositeDataPipeline.TIME_STEPS())
```

### Links to VTK Documentation, Examples, or Class Definitions.

An example can be found in the code snippet I provided.

### Pseudocode or Screenshots

```python
import pyvista as pv
import numpy as np
import vtk

file_path = "file.xdmf"

# Create an Xdmf reader
pv_reader = pv.XdmfReader(path=file_path)

# Get the field names
field_names = pv_reader.point_array_names

# Get the output information
info = pv_reader.reader.GetOutputInformation(0)

# Get the time steps
timestamps = info.Get(vtk.vtkCompositeDataPipeline.TIME_STEPS())
```

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/5332?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#5332](https://app.codecov.io/gh/pyvista/pyvista/pull/5332?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (d3621eb) into [main](https://app.codecov.io/gh/pyvista/pyvista/commit/a150a95ff388ade0ecdc6ec8aaf08e71644db88e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (a150a95) will **decrease** coverage by `7.12%`.
> The diff coverage is `100.00%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5332      +/-   ##
==========================================
- Coverage   96.20%   89.09%   -7.12%     
==========================================
  Files         134      134              
  Lines       22718    22723       +5     
==========================================
- Hits        21856    20245    -1611     
- Misses        862     2478    +1616     
```

</details>
tkoyama010: github-actions preview
MatthewFlamm: If you can get the time steps in a list you can probably get this reader to inherit from `TimeReader`.
MatthewFlamm: We should discourage using the pipeline until we figure out whether we will use it for something in Pyvista, but I think this just gets the needed key to get the info.  So it seems okay.
github-actions[bot]: <!-- NETLIFY DEPLOY COMMENT GENERATED BY ACTIONS_NETLIFY - APP ID SHA256: fa806a2cf6952a12e6174f41068bd77e8b2eec522017ddd7190a1bd99a7881db -->
🚀 Deployed on https://65790588ae018d32cf922c3c--tubular-marzipan-5e01ed.netlify.app
tkoyama010: github-actions preview

Patch:
diff --git a/pyvista/core/_vtk_core.py b/pyvista/core/_vtk_core.py
index 1f41824b..2c31015a 100644
--- a/pyvista/core/_vtk_core.py
+++ b/pyvista/core/_vtk_core.py
@@ -205,6 +205,7 @@ except ImportError:  # pragma: no cover
 from vtkmodules.vtkCommonExecutionModel import (
     vtkAlgorithm,
     vtkAlgorithmOutput,
+    vtkCompositeDataPipeline,
     vtkImageToStructuredGrid,
 )
 from vtkmodules.vtkCommonMath import vtkMatrix3x3, vtkMatrix4x4
diff --git a/pyvista/core/utilities/reader.py b/pyvista/core/utilities/reader.py
index 26cfe12c..7284c61a 100644
--- a/pyvista/core/utilities/reader.py
+++ b/pyvista/core/utilities/reader.py
@@ -12,6 +12,7 @@ from xml.etree import ElementTree
 import numpy as np
 
 import pyvista
+from pyvista.core import _vtk_core as _vtk
 
 from .fileio import _get_ext_force, _process_filename
 from .helpers import wrap
@@ -2406,7 +2407,7 @@ class GIFReader(BaseReader):
     _class_reader = _GIFReader
 
 
-class XdmfReader(BaseReader, PointCellDataSelection):
+class XdmfReader(BaseReader, PointCellDataSelection, TimeReader):
     """XdmfReader for .xdmf files.
 
     Notes
@@ -2447,8 +2448,36 @@ class XdmfReader(BaseReader, PointCellDataSelection):
         return self.reader.GetNumberOfGrids()
 
     def set_active_time_value(self, time_value):  # noqa: D102
+        if time_value not in self.time_values:
+            raise ValueError(
+                f"Not a valid time {time_value} from available time values: {self.time_values}"
+            )
+        self._active_time_value = time_value
         self.reader.UpdateTimeStep(time_value)
 
+    @property
+    def number_time_points(self):  # noqa: D102
+        return len(self.time_values)
+
+    def time_point_value(self, time_point):  # noqa: D102
+        return self.time_values[time_point]
+
+    @property
+    def time_values(self):  # noqa: D102
+        info = self.reader.GetOutputInformation(0)
+        return list(info.Get(_vtk.vtkCompositeDataPipeline.TIME_STEPS()))
+
+    @property
+    def active_time_value(self):  # noqa: D102
+        return self._active_time_value
+
+    def set_active_time_point(self, time_point):  # noqa: D102
+        self.set_active_time_value(self.time_values[time_point])
+
+    def _set_defaults_post(self):
+        self._active_time_value = self.time_values[0]
+        self.set_active_time_value(self._active_time_value)
+
 
 CLASS_READERS = {
     # Standard dataset readers:


Test patch:
diff --git a/tests/core/test_reader.py b/tests/core/test_reader.py
index f88daeb7..48146739 100644
--- a/tests/core/test_reader.py
+++ b/tests/core/test_reader.py
@@ -916,6 +916,11 @@ def test_xdmf_reader():
     assert isinstance(reader, pv.XdmfReader)
     assert reader.path == filename
 
+    assert reader.number_time_points == 5
+    assert reader.time_values == [0.0, 0.25, 0.5, 0.75, 1.0]
+    assert reader.time_point_value(0) == 0.0
+    assert reader.time_point_value(1) == 0.25
+
     assert reader.number_grids == 6
     assert reader.number_point_arrays == 2
 
@@ -923,20 +928,31 @@ def test_xdmf_reader():
     assert reader.cell_array_names == ['a']
 
     blocks = reader.read()
+    assert reader.active_time_value == 0.0
     assert np.array_equal(blocks['TimeSeries_meshio']['phi'], np.array([0.0, 0.0, 0.0, 0.0]))
     reader.set_active_time_value(0.25)
+    assert reader.active_time_value == 0.25
     blocks = reader.read()
     assert np.array_equal(blocks['TimeSeries_meshio']['phi'], np.array([0.25, 0.25, 0.25, 0.25]))
     reader.set_active_time_value(0.5)
+    assert reader.active_time_value == 0.5
     blocks = reader.read()
     assert np.array_equal(blocks['TimeSeries_meshio']['phi'], np.array([0.5, 0.5, 0.5, 0.5]))
     reader.set_active_time_value(0.75)
+    assert reader.active_time_value == 0.75
     blocks = reader.read()
     assert np.array_equal(blocks['TimeSeries_meshio']['phi'], np.array([0.75, 0.75, 0.75, 0.75]))
     reader.set_active_time_value(1.0)
+    assert reader.active_time_value == 1.0
     blocks = reader.read()
     assert np.array_equal(blocks['TimeSeries_meshio']['phi'], np.array([1.0, 1.0, 1.0, 1.0]))
 
+    reader.set_active_time_point(0)
+    assert reader.active_time_value == 0.0
+
+    with pytest.raises(ValueError, match="Not a valid time"):
+        reader.set_active_time_value(1000.0)
+
 
 @pytest.mark.skipif(not HAS_IMAGEIO, reason="Requires imageio")
 def test_try_imageio_imread():